#include <linux/module.h>
#include <linux/init.h>
#include <linux/dma-mapping.h>
#include <linux/err.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/kmod.h>

#include "ei_os.h"
#include "os_inner.h"

#define MULTIMEDIA_NAME_SIZE	20
#define MULTIMEDIA_MODULE_PREFIX	"eimedia"

static int eimedia_match(struct device *dev, struct device_driver *drv)
{
	struct eimedia_device *pdev = to_eimedia_device(dev);

	return (strcmp(pdev->devfs_name, drv->name) == 0);
}

static int eimedia_uevent(struct device *dev, struct kobj_uevent_env *env)
{
	struct eimedia_device *pdev = to_eimedia_device(dev);

	add_uevent_var(env, "MODALIAS=%s:%s", MULTIMEDIA_MODULE_PREFIX,
		       pdev->devfs_name);
	return 0;
}

static int eimedia_dma_configure(struct device *dev)
{
	int ret = 0;

	return ret;
}

static int eimedia_pm_prepare(struct device *dev)
{
	struct eimedia_device *pdev = to_eimedia_device(dev);
	struct eimedia_driver *pdrv = to_eimedia_driver(dev->driver);

	if ((pdrv->pmops == NULL) || (pdrv->pmops->pm_prepare == NULL))
		return 0;

	return pdrv->pmops->pm_prepare(pdev);
}

static void eimedia_pm_complete(struct device *dev)
{
	struct eimedia_device *pdev = to_eimedia_device(dev);
	struct eimedia_driver *pdrv = to_eimedia_driver(dev->driver);

	if ((pdrv->pmops == NULL) || (pdrv->pmops->pm_complete == NULL))
		return;

	pdrv->pmops->pm_complete(pdev);
}

#ifdef CONFIG_SUSPEND
static int eimedia_pm_suspend(struct device *dev)
{
	struct eimedia_device *pdev = to_eimedia_device(dev);
	struct eimedia_driver *pdrv = to_eimedia_driver(dev->driver);

	if ((pdrv->pmops == NULL) || (pdrv->pmops->pm_suspend == NULL))
		return 0;

	return pdrv->pmops->pm_suspend(pdev);
}

static int eimedia_pm_resume(struct device *dev)
{
	struct eimedia_device *pdev = to_eimedia_device(dev);
	struct eimedia_driver *pdrv = to_eimedia_driver(dev->driver);

	if ((pdrv->pmops == NULL) || (pdrv->pmops->pm_resume == NULL))
		return 0;

	return pdrv->pmops->pm_resume(pdev);
}

static int eimedia_pm_freeze(struct device *dev)
{
	struct eimedia_device *pdev = to_eimedia_device(dev);
	struct eimedia_driver *pdrv = to_eimedia_driver(dev->driver);

	if ((pdrv->pmops == NULL) || (pdrv->pmops->pm_freeze == NULL))
		return 0;

	return pdrv->pmops->pm_freeze(pdev);
}

static int eimedia_pm_restore(struct device *dev)
{
	struct eimedia_device *pdev = to_eimedia_device(dev);
	struct eimedia_driver *pdrv = to_eimedia_driver(dev->driver);

	if ((pdrv->pmops == NULL) || (pdrv->pmops->pm_restore == NULL))
		return 0;

	return pdrv->pmops->pm_restore(pdev);
}
#endif

static const struct dev_pm_ops eimedia_bus_pm_ops = {
	.prepare = eimedia_pm_prepare,
	.complete = eimedia_pm_complete,
#ifdef CONFIG_SUSPEND
	.suspend = eimedia_pm_suspend,
	.resume = eimedia_pm_resume,
	.freeze = eimedia_pm_freeze,
	.restore = eimedia_pm_restore,
#endif
};

static void eimedia_bus_release(struct device *dev)
{
}

struct bus_type eimedia_bus_type = {
	.name = "eimedia",
	.match = eimedia_match,
	.uevent = eimedia_uevent,
	.dma_configure = eimedia_dma_configure,
	.pm = &eimedia_bus_pm_ops,
};
EXPORT_SYMBOL(eimedia_bus_type);

struct device eimedia_bus = {
	.init_name = "eimedia",
	.release = eimedia_bus_release,
};
EXPORT_SYMBOL(eimedia_bus);

int eimedia_bus_init(void)
{
	int ret = 0;

	ret = device_register(&eimedia_bus);
	if (ret) {
		os_printk("device register fail\n");
		return ret;
	}

	ret = bus_register(&eimedia_bus_type);
	if (ret) {
		os_printk("bus register fail\n");
		goto error;
	}

	os_printk("%s ok\n", __func__);

	return 0;

error:
	device_unregister(&eimedia_bus);
	return ret;
}

void eimedia_bus_exit(void)
{
	bus_unregister(&eimedia_bus_type);
	device_unregister(&eimedia_bus);
}

static OSAL_LIST_HEAD(eimedia_list);
static DEFINE_MUTEX(eimedia_mtx);

static int eimedia_open(struct inode *inode, struct file *file)
{
	int minor = iminor(inode);
	struct eimedia_device *c = NULL;
	int err = -ENODEV;
	const struct file_operations *new_fops = NULL;

	mutex_lock(&eimedia_mtx);

	os_list_for_each_entry(c, &eimedia_list, list) {
		if (c->minor == minor) {
			new_fops = fops_get(c->fops);
			break;
		}
	}

	if (new_fops == NULL) {
		mutex_unlock(&eimedia_mtx);
		request_module("char-major-%d-%d", EIMEDIA_DEVICE_MAJOR,
			       minor);
		mutex_lock(&eimedia_mtx);

		os_list_for_each_entry(c, &eimedia_list, list) {
			if (c->minor == minor) {
				new_fops = fops_get(c->fops);
				break;
			}
		}

		if (new_fops == NULL)
			goto fail;
	}

	/*
	 * Place the multimeida device in the file's
	 * private_data so it can be used by the
	 * file operations, including f_op->open below
	 */
	file->private_data = c;

	err = 0;
	replace_fops(file, new_fops);
	if (file->f_op->open)
		err = file->f_op->open(inode, file);

fail:
	mutex_unlock(&eimedia_mtx);
	return err;
}

static const struct file_operations eimedia_fops = {
	.owner = THIS_MODULE,
	.open = eimedia_open,
};

int eimedia_register(struct eimedia_device *eimedia)
{
	struct eimedia_device *ptmp = NULL;
	struct eimedia_driver *pdrv = NULL;

	int err = 0;

	mutex_lock(&eimedia_mtx);

	os_list_for_each_entry(ptmp, &eimedia_list, list) {
		if (ptmp->minor == eimedia->minor) {
			mutex_unlock(&eimedia_mtx);

			os_printk("%s eimedia_register fail, minor used\n",
				eimedia->devfs_name);
			return -EBUSY;
		}
	}

	err = eimedia_device_register(eimedia);
	if (err < 0) {
		goto out;
	}

	pdrv =
	    eimedia_driver_register(eimedia->devfs_name, eimedia->owner,
				    eimedia->pmops);
	if (IS_ERR(pdrv)) {
		eimedia_device_deregister(eimedia);

		err = PTR_ERR(pdrv);
		goto out;
	}

	eimedia->driver = pdrv;

	os_list_add(&eimedia->list, &eimedia_list);

out:
	mutex_unlock(&eimedia_mtx);
	return err;
}
EXPORT_SYMBOL(eimedia_register);

int eimedia_deregister(struct eimedia_device *eimedia)
{
	struct eimedia_device *ptmp = NULL, *_ptmp = NULL;

	if (WARN_ON(os_list_empty(&eimedia->list)))
		return -EINVAL;

	mutex_lock(&eimedia_mtx);

	os_list_for_each_entry_safe(ptmp, _ptmp, &eimedia_list, list) {
		if (ptmp->minor == eimedia->minor) {
			os_list_del(&eimedia->list);

			eimedia_driver_deregister(eimedia->driver);
			eimedia->driver = NULL;
			eimedia_device_deregister(eimedia);

			break;
		}
	}

	mutex_unlock(&eimedia_mtx);

	return 0;
}
EXPORT_SYMBOL(eimedia_deregister);

struct eimedia_driver *eimedia_driver_register(const char *name,
					       struct module *owner,
					       struct eimedia_pm_ops *pmops)
{
	int ret;
	struct eimedia_driver *pdrv = NULL;

	if ((name == NULL) || (owner == NULL))
		return ERR_PTR(-EINVAL);

	pdrv = os_kzalloc(sizeof(struct eimedia_driver), os_gfp_kernel);
	if (pdrv == NULL)
		return ERR_PTR(-ENOMEM);

	strncpy(pdrv->name, name, strnlen(name, MULTIMIDIA_MAX_DEV_NAME_LEN));

	pdrv->pmops = pmops;

	pdrv->driver.name = pdrv->name;
	pdrv->driver.owner = owner;
	pdrv->driver.bus = &eimedia_bus_type;

	ret = driver_register(&pdrv->driver);
	if (ret) {
		os_kfree(pdrv);
		return ERR_PTR(ret);
	}

	return pdrv;
}

void eimedia_driver_deregister(struct eimedia_driver *pdrv)
{
	if (pdrv != NULL) {
		driver_unregister(&pdrv->driver);
		os_kfree(pdrv);
	}
}

static void eimedia_device_release(struct device *dev)
{
}

int eimedia_device_register(struct eimedia_device *pdev)
{
	dev_set_name(&pdev->device, "%s", pdev->devfs_name);

	pdev->device.devt = MKDEV(EIMEDIA_DEVICE_MAJOR, pdev->minor);
	pdev->device.release = eimedia_device_release;
	pdev->device.bus = &eimedia_bus_type;

	return device_register(&pdev->device);
}

void eimedia_device_deregister(struct eimedia_device *pdev)
{
	device_unregister(&pdev->device);
}

int eimedia_device_init(void)
{
	int ret = -1;

	if (register_chrdev(EIMEDIA_DEVICE_MAJOR, "eimedia", &eimedia_fops)) {
		os_printk("register_chrdev fail!\n");
		goto err0;
	}

	os_printk("%s ok\n", __func__);

	return 0;

err0:
	return ret;
}

void eimedia_device_exit(void)
{
	if (!os_list_empty(&eimedia_list)) {
		os_printk("%s fail: sub module is still on\n", __func__);
		return;
	}

	unregister_chrdev(EIMEDIA_DEVICE_MAJOR, "eimedia");

	os_printk("%s ok\n", __func__);
}

